001 /* 002 * Copyright 2003-2005 The Apache Software Foundation 003 * Copyright 2005 Stephen McConnell 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package net.dpml.cli.util; 018 019 import java.io.IOException; 020 import java.io.PrintWriter; 021 import java.io.Writer; 022 023 import java.util.ArrayList; 024 import java.util.Collections; 025 import java.util.Comparator; 026 import java.util.HashSet; 027 import java.util.Iterator; 028 import java.util.List; 029 import java.util.Set; 030 031 import net.dpml.cli.DisplaySetting; 032 import net.dpml.cli.Group; 033 import net.dpml.cli.HelpLine; 034 import net.dpml.cli.Option; 035 import net.dpml.cli.OptionException; 036 import net.dpml.cli.resource.ResourceConstants; 037 import net.dpml.cli.resource.ResourceHelper; 038 039 /** 040 * Presents on screen help based on the application's Options 041 * 042 * @author <a href="http://www.dpml.net">Digital Product Meta Library</a> 043 * @version 1.0.0 044 */ 045 public class HelpFormatter 046 { 047 /** 048 * The default screen width 049 */ 050 public static final int DEFAULT_FULL_WIDTH = 80; 051 052 /** 053 * The default minimum description width. 054 */ 055 public static final int DEFAULT_DESCRIPTION_WIDTH = -1; 056 057 /** 058 * The default screen furniture left of screen 059 */ 060 public static final String DEFAULT_GUTTER_LEFT = ""; 061 062 /** 063 * The default screen furniture right of screen 064 */ 065 public static final String DEFAULT_GUTTER_CENTER = " "; 066 067 /** 068 * The default screen furniture between columns 069 */ 070 public static final String DEFAULT_GUTTER_RIGHT = ""; 071 072 /** 073 * The default DisplaySettings used to select the elements to display in the 074 * displayed line of full usage information. 075 * 076 * @see DisplaySetting 077 */ 078 public static final Set DEFAULT_FULL_USAGE_SETTINGS; 079 080 /** 081 * The default DisplaySettings used to select the elements of usage per help 082 * line in the main body of help 083 * 084 * @see DisplaySetting 085 */ 086 public static final Set DEFAULT_LINE_USAGE_SETTINGS; 087 088 /** 089 * The default DisplaySettings used to select the help lines in the main 090 * body of help 091 */ 092 public static final Set DEFAULT_DISPLAY_USAGE_SETTINGS; 093 094 static 095 { 096 final Set fullUsage = new HashSet( DisplaySetting.ALL ); 097 fullUsage.remove( DisplaySetting.DISPLAY_ALIASES ); 098 fullUsage.remove( DisplaySetting.DISPLAY_GROUP_NAME ); 099 DEFAULT_FULL_USAGE_SETTINGS = Collections.unmodifiableSet( fullUsage ); 100 101 final Set lineUsage = new HashSet(); 102 lineUsage.add( DisplaySetting.DISPLAY_ALIASES ); 103 lineUsage.add( DisplaySetting.DISPLAY_GROUP_NAME ); 104 lineUsage.add( DisplaySetting.DISPLAY_PARENT_ARGUMENT ); 105 DEFAULT_LINE_USAGE_SETTINGS = Collections.unmodifiableSet( lineUsage ); 106 107 final Set displayUsage = new HashSet( DisplaySetting.ALL ); 108 displayUsage.remove( DisplaySetting.DISPLAY_PARENT_ARGUMENT ); 109 DEFAULT_DISPLAY_USAGE_SETTINGS = Collections.unmodifiableSet( displayUsage ); 110 } 111 112 private Set m_fullUsageSettings = new HashSet( DEFAULT_FULL_USAGE_SETTINGS ); 113 private Set m_lineUsageSettings = new HashSet( DEFAULT_LINE_USAGE_SETTINGS ); 114 private Set m_displaySettings = new HashSet( DEFAULT_DISPLAY_USAGE_SETTINGS ); 115 private OptionException m_exception = null; 116 private Group m_group; 117 private Comparator m_comparator = null; 118 private String m_divider = null; 119 private String m_header = null; 120 private String m_footer = null; 121 private String m_shellCommand = ""; 122 private PrintWriter m_out = new PrintWriter( System.out ); 123 124 //or should this default to .err? 125 private final String m_gutterLeft; 126 private final String m_gutterCenter; 127 private final String m_gutterRight; 128 private final int m_pageWidth; 129 private final int m_descriptionWidth; 130 131 /** 132 * Creates a new HelpFormatter using the defaults 133 */ 134 public HelpFormatter() 135 { 136 this( 137 DEFAULT_GUTTER_LEFT, DEFAULT_GUTTER_CENTER, DEFAULT_GUTTER_RIGHT, 138 DEFAULT_FULL_WIDTH, DEFAULT_DESCRIPTION_WIDTH ); 139 } 140 141 /** 142 * Creates a new HelpFormatter using the specified parameters 143 * @param gutterLeft the string marking left of screen 144 * @param gutterCenter the string marking center of screen 145 * @param gutterRight the string marking right of screen 146 * @param fullWidth the width of the screen 147 */ 148 public HelpFormatter( 149 final String gutterLeft, final String gutterCenter, final String gutterRight, 150 final int fullWidth ) 151 { 152 this( gutterLeft, gutterCenter, gutterRight, fullWidth, DEFAULT_DESCRIPTION_WIDTH ); 153 } 154 155 /** 156 * Creates a new HelpFormatter using the specified parameters 157 * @param gutterLeft the string marking left of screen 158 * @param gutterCenter the string marking center of screen 159 * @param gutterRight the string marking right of screen 160 * @param fullWidth the width of the screen 161 * @param descriptionWidth the minimum description width 162 */ 163 public HelpFormatter( 164 final String gutterLeft, final String gutterCenter, final String gutterRight, 165 final int fullWidth, final int descriptionWidth ) 166 { 167 // default the left gutter to empty string 168 if( null == gutterLeft ) 169 { 170 m_gutterLeft = DEFAULT_GUTTER_LEFT; 171 } 172 else 173 { 174 m_gutterLeft = gutterLeft; 175 } 176 177 if( null == gutterCenter ) 178 { 179 m_gutterCenter = DEFAULT_GUTTER_CENTER; 180 } 181 else 182 { 183 m_gutterCenter = gutterCenter; 184 } 185 186 if( null == gutterRight ) 187 { 188 m_gutterRight = DEFAULT_GUTTER_RIGHT; 189 } 190 else 191 { 192 m_gutterRight = gutterRight; 193 } 194 195 m_descriptionWidth = descriptionWidth; 196 197 // calculate the available page width 198 m_pageWidth = fullWidth - m_gutterLeft.length() - m_gutterRight.length(); 199 200 // check available page width is valid 201 int availableWidth = fullWidth - m_pageWidth + m_gutterCenter.length(); 202 203 if( availableWidth < 2 ) 204 { 205 throw new IllegalArgumentException( 206 ResourceHelper.getResourceHelper().getMessage( 207 ResourceConstants.HELPFORMATTER_GUTTER_TOO_LONG ) ); 208 } 209 } 210 211 /** 212 * Prints the Option help. 213 * @throws IOException if an error occurs 214 */ 215 public void print() throws IOException 216 { 217 printHeader(); 218 printException(); 219 printUsage(); 220 printHelp(); 221 printFooter(); 222 m_out.flush(); 223 } 224 225 /** 226 * Prints any error message. 227 * @throws IOException if an error occurs 228 */ 229 public void printException() throws IOException 230 { 231 if( m_exception != null ) 232 { 233 printDivider(); 234 printWrapped( m_exception.getMessage() ); 235 } 236 } 237 238 /** 239 * Prints detailed help per option. 240 * @throws IOException if an error occurs 241 */ 242 public void printHelp() throws IOException 243 { 244 printDivider(); 245 final Option option; 246 if( ( m_exception != null ) && ( m_exception.getOption() != null ) ) 247 { 248 option = m_exception.getOption(); 249 } 250 else 251 { 252 option = m_group; 253 } 254 255 // grab the HelpLines to display 256 final List helpLines = option.helpLines( 0, m_displaySettings, m_comparator ); 257 258 // calculate the maximum width of the usage strings 259 int usageWidth = 0; 260 261 for( final Iterator i = helpLines.iterator(); i.hasNext();) 262 { 263 final HelpLine helpLine = (HelpLine) i.next(); 264 final String usage = helpLine.usage( m_lineUsageSettings, m_comparator ); 265 usageWidth = Math.max( usageWidth, usage.length() ); 266 } 267 268 // 269 // add check for an overriding description max width (needed in complex 270 // usage scenarios) 271 // 272 273 if( m_descriptionWidth > -1 ) 274 { 275 int max = m_pageWidth - m_descriptionWidth; 276 if( usageWidth > max ) 277 { 278 usageWidth = max; 279 } 280 } 281 282 // build a blank string to pad wrapped descriptions 283 final StringBuffer blankBuffer = new StringBuffer(); 284 285 for( int i = 0; i < usageWidth; i++ ) 286 { 287 blankBuffer.append( ' ' ); 288 } 289 290 // determine the width available for descriptions 291 final int descriptionWidth = 292 Math.max( 1, m_pageWidth - m_gutterCenter.length() - usageWidth ); 293 294 // display each HelpLine 295 for( final Iterator i = helpLines.iterator(); i.hasNext();) 296 { 297 // grab the HelpLine 298 final HelpLine helpLine = (HelpLine) i.next(); 299 300 // wrap the description 301 final List descList = wrap( helpLine.getDescription(), descriptionWidth ); 302 final Iterator descriptionIterator = descList.iterator(); 303 304 // display usage + first line of description 305 printGutterLeft(); 306 pad( helpLine.usage( m_lineUsageSettings, m_comparator ), usageWidth, m_out ); 307 m_out.print( m_gutterCenter ); 308 pad( (String) descriptionIterator.next(), descriptionWidth, m_out ); 309 printGutterRight(); 310 m_out.println(); 311 312 // display padding + remaining lines of description 313 while( descriptionIterator.hasNext() ) 314 { 315 printGutterLeft(); 316 317 //pad(helpLine.getUsage(),usageWidth,m_out); 318 m_out.print( blankBuffer ); 319 m_out.print( m_gutterCenter ); 320 pad( (String) descriptionIterator.next(), descriptionWidth, m_out ); 321 printGutterRight(); 322 m_out.println(); 323 } 324 } 325 printDivider(); 326 } 327 328 /** 329 * Prints a single line of usage information (wrapping if necessary) 330 * @throws IOException if an error occurs 331 */ 332 public void printUsage() throws IOException 333 { 334 printDivider(); 335 final StringBuffer buffer = new StringBuffer( "Usage:\n" ); 336 buffer.append( m_shellCommand ).append( ' ' ); 337 String separator = getSeparator(); 338 m_group.appendUsage( buffer, m_fullUsageSettings, m_comparator, separator ); 339 printWrapped( buffer.toString() ); 340 } 341 342 private String getSeparator() 343 { 344 if( m_group.getMaximum() == 1 ) 345 { 346 return " | "; 347 } 348 else 349 { 350 return " "; 351 } 352 } 353 354 /** 355 * Prints a m_header string if necessary 356 * @throws IOException if an error occurs 357 */ 358 public void printHeader() throws IOException 359 { 360 if( m_header != null ) 361 { 362 printDivider(); 363 printWrapped( m_header ); 364 } 365 } 366 367 /** 368 * Prints a m_footer string if necessary 369 * @throws IOException if an error occurs 370 */ 371 public void printFooter() throws IOException 372 { 373 if( m_footer != null ) 374 { 375 printWrapped( m_footer ); 376 printDivider(); 377 } 378 } 379 380 /** 381 * Prints a string wrapped if necessary 382 * @param text the string to wrap 383 * @throws IOException if an error occurs 384 */ 385 protected void printWrapped( final String text ) throws IOException 386 { 387 for( final Iterator i = wrap( text, m_pageWidth ).iterator(); i.hasNext();) 388 { 389 printGutterLeft(); 390 pad( (String) i.next(), m_pageWidth, m_out ); 391 printGutterRight(); 392 m_out.println(); 393 } 394 } 395 396 /** 397 * Prints the left gutter string 398 */ 399 public void printGutterLeft() 400 { 401 if( m_gutterLeft != null ) 402 { 403 m_out.print( m_gutterLeft ); 404 } 405 } 406 407 /** 408 * Prints the right gutter string 409 */ 410 public void printGutterRight() 411 { 412 if( m_gutterRight != null ) 413 { 414 m_out.print( m_gutterRight ); 415 } 416 } 417 418 /** 419 * Prints the m_divider text 420 */ 421 public void printDivider() 422 { 423 if( m_divider != null ) 424 { 425 m_out.println( m_divider ); 426 } 427 } 428 429 /** 430 * Pad the supplied string. 431 * @param text the text to pad 432 * @param width the padding width 433 * @param writer the writer 434 * @exception IOException if an I/O error occurs 435 */ 436 protected static void pad( 437 final String text, final int width, final Writer writer ) 438 throws IOException 439 { 440 final int left; 441 442 // write the text and record how many characters written 443 if ( text == null ) 444 { 445 left = 0; 446 } 447 else 448 { 449 writer.write( text ); 450 left = text.length(); 451 } 452 453 // pad remainder with spaces 454 for( int i = left; i < width; ++i ) 455 { 456 writer.write( ' ' ); 457 } 458 } 459 460 /** 461 * Return a list of strings resulting from the wrapping of a supplied 462 * target string. 463 * @param text the target string to wrap 464 * @param width the wrappping width 465 * @return the list of wrapped fragments 466 */ 467 protected static List wrap( final String text, final int width ) 468 { 469 // check for valid width 470 if( width < 1 ) 471 { 472 throw new IllegalArgumentException( 473 ResourceHelper.getResourceHelper().getMessage( 474 ResourceConstants.HELPFORMATTER_WIDTH_TOO_NARROW, 475 new Object[]{new Integer( width )} ) ); 476 } 477 478 // handle degenerate case 479 if( text == null ) 480 { 481 return Collections.singletonList( "" ); 482 } 483 484 final List lines = new ArrayList(); 485 final char[] chars = text.toCharArray(); 486 int left = 0; 487 488 // for each character in the string 489 while( left < chars.length ) 490 { 491 // sync left and right indeces 492 int right = left; 493 494 // move right until we run m_out of characters, width or find a newline 495 while( 496 ( right < chars.length ) 497 && ( chars[right] != '\n' ) 498 && ( right < ( left + width + 1 ) ) ) 499 { 500 right++; 501 } 502 503 // if a newline was found 504 if( ( right < chars.length ) && ( chars[right] == '\n' ) ) 505 { 506 // record the substring 507 final String line = new String( chars, left, right - left ); 508 lines.add( line ); 509 510 // move to the end of the substring 511 left = right + 1; 512 513 if( left == chars.length ) 514 { 515 lines.add( "" ); 516 } 517 518 // restart the loop 519 continue; 520 } 521 522 // move to the next ideal wrap point 523 right = ( left + width ) - 1; 524 525 // if we have run m_out of characters 526 if( chars.length <= right ) 527 { 528 // record the substring 529 final String line = new String( chars, left, chars.length - left ); 530 lines.add( line ); 531 532 // abort the loop 533 break; 534 } 535 536 // back track the substring end until a space is found 537 while( ( right >= left ) && ( chars[right] != ' ' ) ) 538 { 539 right--; 540 } 541 542 // if a space was found 543 if( right >= left ) 544 { 545 // record the substring to space 546 final String line = new String( chars, left, right - left ); 547 lines.add( line ); 548 549 // absorb all the spaces before next substring 550 while( ( right < chars.length ) && ( chars[right] == ' ' ) ) 551 { 552 right++; 553 } 554 555 left = right; 556 557 // restart the loop 558 continue; 559 } 560 561 // move to the wrap position irrespective of spaces 562 right = Math.min( left + width, chars.length ); 563 564 // record the substring 565 final String line = new String( chars, left, right - left ); 566 lines.add( line ); 567 568 // absorb any the spaces before next substring 569 while( ( right < chars.length ) && ( chars[right] == ' ' ) ) 570 { 571 right++; 572 } 573 574 left = right; 575 } 576 577 return lines; 578 } 579 580 /** 581 * The Comparator to use when sorting Options 582 * @param comparator Comparator to use when sorting Options 583 */ 584 public void setComparator( Comparator comparator ) 585 { 586 m_comparator = comparator; 587 } 588 589 /** 590 * The DisplaySettings used to select the help lines in the main body of 591 * help 592 * 593 * @param displaySettings the settings to use 594 * @see DisplaySetting 595 */ 596 public void setDisplaySettings( Set displaySettings ) 597 { 598 m_displaySettings = displaySettings; 599 } 600 601 /** 602 * Sets the string to use as a m_divider between sections of help 603 * @param divider the dividing string 604 */ 605 public void setDivider( String divider ) 606 { 607 m_divider = divider; 608 } 609 610 /** 611 * Sets the exception to document 612 * @param exception the exception that occured 613 */ 614 public void setException( OptionException exception ) 615 { 616 m_exception = exception; 617 } 618 619 /** 620 * Sets the footer text of the help screen 621 * @param footer the footer text 622 */ 623 public void setFooter( String footer ) 624 { 625 m_footer = footer; 626 } 627 628 /** 629 * The DisplaySettings used to select the elements to display in the 630 * displayed line of full usage information. 631 * @see DisplaySetting 632 * @param fullUsageSettings the full usage settings 633 */ 634 public void setFullUsageSettings( Set fullUsageSettings ) 635 { 636 m_fullUsageSettings = fullUsageSettings; 637 } 638 639 /** 640 * Sets the Group of Options to document 641 * @param group the options to document 642 */ 643 public void setGroup( Group group ) 644 { 645 m_group = group; 646 } 647 648 /** 649 * Sets the header text of the help screen 650 * @param header the m_footer text 651 */ 652 public void setHeader( String header ) 653 { 654 m_header = header; 655 } 656 657 /** 658 * Sets the DisplaySettings used to select elements in the per helpline 659 * usage strings. 660 * @see DisplaySetting 661 * @param lineUsageSettings the DisplaySettings to use 662 */ 663 public void setLineUsageSettings( Set lineUsageSettings ) 664 { 665 m_lineUsageSettings = lineUsageSettings; 666 } 667 668 /** 669 * Sets the command string used to invoke the application 670 * @param shellCommand the invocation command 671 */ 672 public void setShellCommand( String shellCommand ) 673 { 674 m_shellCommand = shellCommand; 675 } 676 677 /** 678 * Return the comparator. 679 * @return the Comparator used to sort the Group 680 */ 681 public Comparator getComparator() 682 { 683 return m_comparator; 684 } 685 686 /** 687 * Return the display settings. 688 * @return the DisplaySettings used to select HelpLines 689 */ 690 public Set getDisplaySettings() 691 { 692 return m_displaySettings; 693 } 694 695 /** 696 * Return the divider. 697 * @return the String used as a horizontal section m_divider 698 */ 699 public String getDivider() 700 { 701 return m_divider; 702 } 703 704 /** 705 * Return the option exception 706 * @return the Exception being documented by this HelpFormatter 707 */ 708 public OptionException getException() 709 { 710 return m_exception; 711 } 712 713 /** 714 * Return the footer text. 715 * @return the help screen footer text 716 */ 717 public String getFooter() 718 { 719 return m_footer; 720 } 721 722 /** 723 * Return the full usage display settings. 724 * @return the DisplaySettings used in the full usage string 725 */ 726 public Set getFullUsageSettings() 727 { 728 return m_fullUsageSettings; 729 } 730 731 /** 732 * Return the group. 733 * @return the group documented by this HelpFormatter 734 */ 735 public Group getGroup() 736 { 737 return m_group; 738 } 739 740 /** 741 * Return the gutter center string. 742 * @return the String used as the central gutter 743 */ 744 public String getGutterCenter() 745 { 746 return m_gutterCenter; 747 } 748 749 /** 750 * Return the gutter left string. 751 * @return the String used as the left gutter 752 */ 753 public String getGutterLeft() 754 { 755 return m_gutterLeft; 756 } 757 758 /** 759 * Return the gutter right string. 760 * @return the String used as the right gutter 761 */ 762 public String getGutterRight() 763 { 764 return m_gutterRight; 765 } 766 767 /** 768 * Return the header string. 769 * @return the help screen header text 770 */ 771 public String getHeader() 772 { 773 return m_header; 774 } 775 776 /** 777 * Return the line usage settings. 778 * @return the DisplaySettings used in the per help line usage strings 779 */ 780 public Set getLineUsageSettings() 781 { 782 return m_lineUsageSettings; 783 } 784 785 /** 786 * Return the page width. 787 * @return the width of the screen in characters 788 */ 789 public int getPageWidth() 790 { 791 return m_pageWidth; 792 } 793 794 /** 795 * Return the shell command. 796 * @return the command used to execute the application 797 */ 798 public String getShellCommand() 799 { 800 return m_shellCommand; 801 } 802 803 /** 804 * Set the print writer. 805 * @param out the PrintWriter to write to 806 */ 807 public void setPrintWriter( PrintWriter out ) 808 { 809 m_out = out; 810 } 811 812 /** 813 * Return the print writer. 814 * @return the PrintWriter that will be written to 815 */ 816 public PrintWriter getPrintWriter() 817 { 818 return m_out; 819 } 820 }